#!/usr/bin/python
#***************************************************************************
# Copyright notice for this file:
#  Copyright (C) 2012 TheAxeMan
#  _______ _                               _    _
# |__   __| |     ___     /\          ___ | \  / |
#    | |  | |__  / _ \   /  \  __  _ / _ \|  \/  | __ _ _ __
#    | |  | '_ \|  __/  / /\ \ \ \/ |  __/| .  . |/ _` | '_ \
#    | |  | | | | (__/\/ ____ \ )  (| (__/| |\/| | (_| | | | |
#    |_|  |_| |_|\____/_/    \_._/\_.\____|_|  |_|\__,_|_| |_|
#
# Final Fantasy TAS scripts by TheAxeMan
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#***************************************************************************


import sys

from battlePlannerLib import BattleRandGenerator
from battlePlannerLib import Combatant
from battlePlannerLib import CharType


class Battle:
  """
  Contains all the information on the current state of a battle
  
  Members:
  rand_engine - BattleRandGenerator object to control results
  enemy_list - list of Combatants representing the enemies
  party_list - list of Combatants representing the party
  """
  #cost values for items in battle
  partyFightCost = 138      #time for party member to attack enemy
  #partyFightCost[CharType.garland] = 160
  #partyFightCost[CharType.pirate] = 138
  #partyFightCost[CharType.wizard] = 138
  partyRunCost = 42         #time for party member to fail run
  enemyFightCost = 89       #time for enemy to attack
  partyRunOrderCost = 41    #time to issue order to fight
  partyFightOrderCost = 42  #time to issue order to run
  roundCost = 40            #additional time for a round of battle

  #number of seeds burned for dust when party hits enemy
  dust_seeds = 54
  #dust_seeds[CharType.garland] = 135
  #dust_seeds[CharType.pirate] = 54
  #dust_seeds[CharType.wizard] = 54
  startingHeroHp = 35

  def __init__(self):
    '''Override to add the right combatants'''
    self.rand_engine = BattleRandGenerator()
    self.enemy_list = [Combatant(0) for i in xrange(9)]
    self.party_list = [Combatant(self.__class__.startingHeroHp) for i in xrange(4)]
    self.orderSeedCosts, self.orderFrameCosts = self.getOrderCosts()
      
  def getState(self):
    '''Override this to simplify, but make sure load matches'''
    return (self.rand_engine.seed, 
            tuple([e.hp for e in self.enemy_list]),
            tuple([p.hp for p in self.party_list]),
            )
      
  def loadState(self, state):
    '''Override this to simplify, but make sure save matches'''
    self.rand_engine.seed = state[0]
    for i, hp in enumerate(state[1]):
      self.enemy_list[i].hp = hp
    for i, hp in enumerate(state[2]):
      self.party_list[i].hp = hp

  def printPartyHp(self):
    for i in xrange(4):
      print self.party_list[i].hp,
    print ""

  def monstersPerished(self):
    #If not all enemies are present this can be simplified
    return not bool(filter(lambda e: e.hp>0, self.enemy_list))
  
  def partyPerished(self):
    return not bool(filter(lambda p: p.hp>0, self.party_list))
  
  def partyMembersLiving(self):
    return len(filter(lambda p: p.hp>0, self.party_list))
  
  def enemiesLiving(self):
    return len(filter(lambda e: e.hp>0, self.enemy_list))
  
  def partyMembersDamaged(self):
    #assumes a starting hp of 28
    return len(filter(lambda p: p.hp<28, self.party_list))
  
  def getLivingEnemyList(self):
    return filter(lambda x: self.enemy_list[x].hp > 0, range(9))

  orderCostLookupTable = {}

  def getOrderCosts(self):
    """Calculates the impact in frames and seeds used to issue orders.
       Returns a list mapping enemy id to seeds used to move cursor.
    """
    #a mapping from the enemy index to the cursor movement index
    #note that the mapping is reflective!
    index_map = [1, 0, 2, 4, 3, 5, 7, 6, 8]
    #make a list of living cursor positions
    alive_list = tuple(filter(lambda i: self.enemy_list[i].hp>0, index_map))
        
    if alive_list in self.__class__.orderCostLookupTable:
      return self.__class__.orderCostLookupTable[alive_list]

    seeds_used = [0 for x in xrange(9)]
    frames_used = [0 for x in xrange(9)]

    #cursor start position: 2 seeds
    seeds_used[alive_list[0]] = 2
    frames_used[alive_list[0]] = 0
    if len(alive_list) == 1: return seeds_used, frames_used
    #move cursor once: 3 seeds
    seeds_used[alive_list[1]] = 3
    seeds_used[alive_list[-1]] = 3
    frames_used[alive_list[1]] = 2
    frames_used[alive_list[-1]] = 2
    if len(alive_list) <= 3: return seeds_used, frames_used
    #move cursor twice: 5 seeds
    seeds_used[alive_list[2]] = 5
    seeds_used[alive_list[-2]] = 5
    frames_used[alive_list[2]] = 6
    frames_used[alive_list[-2]] = 6
    if len(alive_list) <= 5: return seeds_used, frames_used
    #move cursor thrice: 7 seeds
    seeds_used[alive_list[3]] = 7
    seeds_used[alive_list[-3]] = 7
    frames_used[alive_list[3]] = 10
    frames_used[alive_list[-3]] = 10
    if len(alive_list) <= 7: return seeds_used, frames_used
    #move cursor four times: 9 seeds
    seeds_used[alive_list[4]] = 9
    seeds_used[alive_list[-4]] = 9
    frames_used[alive_list[4]] = 14
    frames_used[alive_list[-4]] = 14

    self.__class__.orderCostLookupTable[alive_list] = (tuple(seeds_used), tuple(frames_used))
    return seeds_used, frames_used
  
  def getTarget(self):
    #if self.partyPerished():
      #print "Wait a minute, there aren't any heroes to attack"
      #sys.exit()
    while(True):
      target_hero = self.rand_engine.getTarget()
      if self.party_list[target_hero].hp > 0:
        return target_hero
      
  def enemyAttack(self, enemyIndex, print_attack=False):
    #don't do anything if enemy is dead
    enemy = self.enemy_list[enemyIndex]
    if enemy.hp == 0:
      return 0, False, False

    #first roll determines whether he uses fight, magic or special.
    #but we only handle fight-only enemies
    self.rand_engine.useUpRands(1)
        
    target_hero = self.getTarget()
    #burn off 12 rands for shaking screen
    self.rand_engine.useUpRands(12)
    
    if print_attack:
      print "enemy",enemyIndex,"attacks party member",target_hero
    hero = self.party_list[target_hero]
    enemy.attack(hero, self.rand_engine, print_attack)
  
    if self.party_list[target_hero].hp == 0:
      if print_attack:
        print "slain"
      partyPerished = self.partyPerished()
      if print_attack and partyPerished:
        print "party perished"
    else:
      partyPerished = False
      
    return self.enemyFightCost, partyPerished, True
      
  def heroAttack(self, heroIndex, party_orders, print_attack=False):
    #don't do anything if this hero is dead
    hero = self.party_list[heroIndex]
    if hero.hp == 0:
      return 0, 0, False
    
    target_monster = party_orders[heroIndex]
    
    #Attempt to run and fail
    if target_monster < 0:
      if print_attack:
        print "tried to run..."
      return self.partyRunCost, 0, False
    
    #debugging
    #if self.enemy_list[target_monster].type not in self.partyFightCost:
    #  print "trying to attack a dead enemy? Orders:",party_orders
    #  print "living enemies are:",self.getLivingEnemyList()
    #  sys.exit()

    enemy = self.enemy_list[target_monster]
    #if monster is dead, ineffective
    if enemy.hp == 0:
      if print_attack:
        print "ineffective - no monster there"
      return self.partyFightCost, 0, False
    
    #burn off some rands for dust effect
    self.rand_engine.useUpRands(self.dust_seeds)
    #now attack
    if print_attack:
      print "party member",heroIndex,"attacks enemy",target_monster
    damage = hero.attack(enemy, self.rand_engine, print_attack)
  
    if self.enemy_list[target_monster].hp == 0:
      if print_attack:
        print "terminated"
      monstersPerished = self.monstersPerished()
      if print_attack and monstersPerished:
        print "Monsters perished"
    else:
      monstersPerished = False
          
    return self.partyFightCost, damage, monstersPerished
  
  def simulateActions(self, battle_order, party_orders, print_attack=False):
    cost = 0
    total_damage = 0
    partyPerished = False
    monstersPerished = False
    enemyAttacks = 0
    
    for i in battle_order:
      if i < 80:
        #an enemy's turn
        actionCost, partyPerished, enemyAttacked = self.enemyAttack(i, print_attack)
        cost += actionCost
        enemyAttacks += int(enemyAttacked)
        if partyPerished:
          if print_attack:
            print "party perished"
          break

      else:
        #party member's turn
        actionCost, damage, monstersPerished = self.heroAttack(i-80, party_orders, print_attack)
        total_damage += damage
        cost += actionCost
        
        if monstersPerished:
          break
        
    return cost, total_damage, partyPerished, monstersPerished, enemyAttacks

  def simulateRound(self, party_orders, print_attack=False):
    """
    Simulates a round of battle. party_orders is a list of numbers
    that represents the orders for each party member.
    Return a cost value in frames for the round and damage done by party.
    The rand engine object is not touched until getting the battle order.
    """
    #start out with "other" frame cost for a round
    cost = self.roundCost
    #add up order costs
    for i, order in enumerate(party_orders):
      if self.party_list[i].hp == 0: continue
      if order < 0:
        cost += self.partyRunOrderCost
        self.rand_engine.useUpRands(2)
      else:
        #Frames and seeds used depends on how many times cursor needs to be moved.
        cost += self.partyFightOrderCost + self.orderFrameCosts[order]
        self.rand_engine.useUpRands(self.orderSeedCosts[order])

    battle_order = self.rand_engine.getBattleOrder()
    #print battle_order
  
    actionCost, total_damage, partyPerished, monstersPerished, enemyAttacks = \
              self.simulateActions(battle_order, party_orders, print_attack)
    cost += actionCost

    return cost, total_damage, partyPerished, monstersPerished, enemyAttacks
  
  def getPossibleOrders(self, roundNumber):
    '''This generates all possibilities for the players attacking any enemy or running.
       Override to be smarter.'''
    all_single_orders = [-1] + filter(lambda i: self.enemy_list[i].hp > 0, range(9))
    single_order = [ [-1] if self.party_list[i].hp == 0 else all_single_orders for i in xrange(4) ]
    
    possible_orders = set()
    for a in single_order[0]:
      for b in single_order[1]:
        for c in single_order[2]:
          for d in single_order[3]:
            possible_orders.add( (a,b,c,d) )
    possible_orders.remove( (-1,-1,-1,-1) )
    return possible_orders
  
  def optimize(self):
    #get possible orders
    #simulate with different orders and waits
    #see if result is acceptable
    
    #History is a list of what happened each round. Remember wait seeds and orders.
    history = []
    self.bestResultCommands = None
    self.bestResultCost = 999999999
    
    self.optimizeNextRound(0, history)
    
    if self.bestResultCommands is None:
      print "Unable to get an acceptable result"
    else:
      print "Best result is",self.bestResultCommands,"with cost of",self.bestResultCost
      print "End seed is",self.bestResultEndSeed
    
  def getMaxWaitSeeds(self, roundNumber):
    return 90
    
  def optimizeNextRound(self, costSoFar, history):
    roundNumber = len(history)+1
    roundStartState = self.getState()
    possible_orders = self.getPossibleOrders(roundNumber)
    
    orderSeedCosts, orderFrameCosts = self.getOrderCosts()
    totalSims = len(possible_orders) * (self.getMaxWaitSeeds(roundNumber)+1)
    numSims = 0
    
    for wait in xrange(self.getMaxWaitSeeds(roundNumber)+1):
      for orders in possible_orders:
        self.loadState(roundStartState)
        self.rand_engine.useUpRands(wait)
        
        self.orderSeedCosts = orderSeedCosts
        self.orderFrameCosts = orderFrameCosts
        roundCost, roundDamage, partyPerished, monstersPerished, enemyAttacks = \
                 self.simulateRound(orders)
        totalCost = costSoFar + roundCost + 2*wait
        
        numSims += 1
        if roundNumber == 1 or (roundNumber ==2 and numSims%100==0):
          if roundNumber == 2:
            print " ",
          print "Simulated",numSims,"of",totalSims,"variations on round",roundNumber
        
        if monstersPerished: 
          if totalCost < self.bestResultCost and \
             self.resultIsAcceptable(totalCost):
            self.bestResultCommands = history + [(wait, orders)]
            self.bestResultCost = totalCost
            self.bestResultEndSeed = self.rand_engine.seed
          continue
            
        if partyPerished or not self.roundIsAcceptable(roundNumber, totalCost, roundDamage, enemyAttacks):
          continue
        
        #Recurse
        self.optimizeNextRound(totalCost, history + [(wait, orders)])
    
  def roundIsAcceptable(self, roundNumber, totalCost, roundDamage, enemyAttacks):
    '''Check to see if round is acceptable. Did we do decent damage,
       not die, etc?'''
    raise NotImplementedError()
    
  def resultIsAcceptable(self, totalCost):
    '''Check to see if battle result is acceptable. Did we finish fast enough,
       not die, etc?'''
    raise NotImplementedError()
    

def garland_demo():
  """Demonstrate battle flow for garland fight"""
  garland_fight = Battle()

  garland = Combatant()
  garland.type = CharType.garland
  garland.damage = 15
  garland.defense = 10
  garland.hp = 106
  garland.crit_threshold = 0
  garland.hit_thresholds = {}
  garland.hit_thresholds[CharType.level1fighter] = 142

  #The number is the index of enemy to attack or < 0 for run
  garland_fight.enemy_list[2] = garland
  #this is what the seed is when the battle starts
  garland_fight.rand_engine.seed = 51

  order_list = []
  if 0:
    order_list.append([-1,2,-1,2])
    order_list.append([-1,-1,-1,2])
    order_list.append([2,2,2,-1])
    order_list.append([2,-1,2,2])
    wait_list = [1, 45, 66, 31]
  else:
    order_list.append([-1,2,-1,2])
    order_list.append([2,2,-1,2])
    order_list.append([-1,2,2,2])
    wait_list = [1, 27, 43]

  for i in xrange(len(wait_list)):
    garland_fight.rand_engine.useUpRands(2*garland_fight.partyMembersLiving())
    garland_fight.rand_engine.useUpRands(wait_list[i])

    #r = garland_fight.rand_engine.copy()
    #print r.getBattleOrder()
    #r = garland_fight.rand_engine.copy()
    #for j in xrange(34):
    #  print r.getRandInRange(0, 12),
    #print ""

    garland_fight.simulateRound(order_list[i], True)
    print "seed is",garland_fight.rand_engine.seed
    print ""

def pirate_demo():
  pirate_fight = Battle()

  #the pirates
  for i in xrange(9):
    pirate = Combatant()
    pirate.type = CharType.pirate
    pirate.damage = 8
    pirate.defense = 0
    pirate.hp = 6
    pirate.crit_threshold = 1
    pirate.hit_thresholds = {}
    pirate.hit_thresholds[CharType.level2fighter] = 116
    pirate_fight.enemy_list[i] = pirate

  #place three level 2 fighters
  for i in xrange(3):
    level2fighter = Combatant()
    level2fighter.type = CharType.level2fighter
    level2fighter.damage = 10
    level2fighter.defense = 0
    level2fighter.hp = 35
    level2fighter.crit_threshold = 0
    level2fighter.hit_thresholds = {}
    level2fighter.hit_thresholds[CharType.pirate] = 169
    pirate_fight.party_list[i] = level2fighter

  pirate_fight.party_list[1].hp = 17

  #last party member is dead
  pirate_fight.party_list[3].hp = 0

  #this is what the seed is when the battle starts
  pirate_fight.rand_engine.seed = 49

  order_list = []
  order_list.append([3,7,1,-1])
  order_list.append([4,0,8,-1])
  order_list.append([2,5,6,-1])

  wait_list = [4, 8, 35]

  for i in xrange(len(wait_list)):

    orders = order_list[i]

#    print "seed is",pirate_fight.rand_engine.seed

    order_seed_costs, order_frame_costs = pirate_fight.getOrderCosts()
    #calculate seeds and frames used up giving orders
    seeds_used = wait_list[i]
    for j in xrange(4):
      if pirate_fight.party_list[j].hp > 0 and orders[j] >= 0:
#        print "party member",j,"attacks enemy",orders[j]
#        print "uses up",order_seed_costs[orders[j]],"seeds"
        seeds_used += order_seed_costs[orders[j]]
    pirate_fight.rand_engine.useUpRands(seeds_used)
#    print ""

    #r = pirate_fight.rand_engine.copy()
    #print r.getBattleOrder()

    pirate_fight.simulateRound(orders, True)
    print ""



def wizard_demo():
  wizard_fight = Battle()
  short_sword_guy = 1

  #the wizards
  for i in xrange(2):
    wizard = Combatant()
    wizard.type = CharType.wizard
    wizard.damage = 30
    wizard.num_hits = 2
    wizard.defense = 16
    wizard.hp = 84
    wizard.crit_threshold = 1
    wizard.hit_thresholds = {}
    wizard.hit_thresholds[CharType.level3fighter] = 134
    wizard_fight.enemy_list[i] = wizard

  #place two level 3 fighters
  for i in xrange(2):
    level3fighter = Combatant()
    level3fighter.type = CharType.level3fighter
    level3fighter.damage = 11
    level3fighter.defense = 0
    level3fighter.crit_threshold = 0
    level3fighter.hit_thresholds = {}
    level3fighter.hit_thresholds[CharType.wizard] = 118
    wizard_fight.party_list[i] = level3fighter

  #But the guy with short sword is tougher
  wizard_fight.party_list[short_sword_guy].damage = 26
  wizard_fight.party_list[short_sword_guy].hit_thresholds[CharType.wizard] = 128
  wizard_fight.party_list[short_sword_guy].crit_threshold = 6

  #this covers the current hps
  wizard_fight.party_list[0].hp = 9
  wizard_fight.party_list[1].hp = 19

  #last two party members are dead
  #wizard_fight.party_list[1].hp = 0
  #wizard_fight.party_list[1].status = 1
  wizard_fight.party_list[2].hp = 0
  wizard_fight.party_list[2].hp = 0
  wizard_fight.party_list[3].hp = 0
  wizard_fight.party_list[3].hp = 0

  #this is what the seed is when the battle starts
  wizard_fight.rand_engine.seed = 49

  order_list = []
  order_list.append([-1,0,-1,-1])
  order_list.append([-1,0,-1,-1])
  order_list.append([-1,1,-1,-1])
  order_list.append([-1,1,-1,-1])

  wait_list = [52, 51, 12, 17]

  for i in xrange(len(wait_list)):

    orders = order_list[i]

#    print "seed is",wizard_fight.rand_engine.seed

    order_seed_costs, order_frame_costs = wizard_fight.getOrderCosts()
    #calculate seeds and frames used up giving orders
    seeds_used = wait_list[i]
    for j in xrange(4):
      if wizard_fight.party_list[j].status == 0 and orders[j] >= 0:
#        print "party member",j,"attacks enemy",orders[j]
#        print "uses up",order_seed_costs[orders[j]],"seeds"
        seeds_used += order_seed_costs[orders[j]]
    wizard_fight.rand_engine.useUpRands(seeds_used)
#    print ""

    #r = wizard_fight.rand_engine.copy()
    #print r.getBattleOrder()

    wizard_fight.simulateRound(orders, True)
    print ""

    if wizard_fight.partyPerished(): break
    if wizard_fight.monstersPerished(): break

if __name__ == "__main__":
  #garland_demo()
  pirate_demo()
  #wizard_demo()

